Introdução a tipos em Julia

Os tipos em Julia são organizados em uma árvore. Na raiz da arvore está o tipo Any e dizemos que Any é supertipo de qualquer tipo em Julia. De forma equivalente, dizemos que todos os tipos dão subtipos de Any:


In [1]:
Any


Out[1]:
Any

In [2]:
subtypes(Any)


Out[2]:
176-element Array{Any,1}:
 AbstractArray{T,N}                                                                                   
 AbstractCmd                                                                                          
 AbstractHeap{VT}                                                                                     
 AbstractREPL                                                                                         
 AbstractRNG                                                                                          
 Accumulator{T,V<:Number}                                                                             
 Algorithm                                                                                            
 Any                                                                                                  
 Associative{K,V}                                                                                     
 AsyncWork                                                                                            
 Available                                                                                            
 BoundingBox                                                                                          
 Box                                                                                                  
 ⋮                                                                                                    
 VersionSet                                                                                           
 VersionWeight                                                                                        
 WeakRef                                                                                              
 Worker                                                                                               
 ZeroOffsetVector                                                                                     
 Zip2{I1,I2}                                                                                          
 Zip{I<:(Any...,)}                                                                                    
 c_CholmodDense{T<:Union(Float64,Complex{Float32},Float32,Complex{Float64})}                          
 c_CholmodFactor{Tv<:Union(Float64,Complex{Float32},Float32,Complex{Float64}),Ti<:Union(Int32,Int64)} 
 c_CholmodSparse{Tv<:Union(Float64,Complex{Float32},Float32,Complex{Float64}),Ti<:Union(Int32,Int64)} 
 c_CholmodTriplet{Tv<:Union(Float64,Complex{Float32},Float32,Complex{Float64}),Ti<:Union(Int32,Int64)}
 dl_phdr_info                                                                                         

Ainda na anologia com uma árvore, cada um de seus nós (incluindo a raiz, Any ) é um tipo abstrato. As folhas da árvore, tipos que não possuem nenhum subtipo, são ditas tipos concretos.

Tipos abstratos não podem ser contruídos, ou seja, nenhum objeto é de um tipo abstrato. Criamos um tipo abstrato fazendo:


In [3]:
abstract Pessoa

Tipos concretos no entanto, não só podem ser construídos, como também possuem campos. Estes campos são onde guardamos os dados do objeto. Isso ficará claro com a definição do tipo concreto Foo:


In [4]:
type Foo
    bar
    número::Int
end

Note que esse tipo possui dois campos: o primeiro é bar e pode ser qualquer tipo de dado; o segundo é número e deve ser obrigatóriamente do tipo Int. Mas como construimos um objeto do tipo Foo? Por padrão, Julia cria uma função com o mesmo nome do tipo, cujos parametros são referentes a cada campo. Exemplo:


In [5]:
a = Foo("Texto", 1234)
println(typeof(a))


Foo

Futuramente mostraremos como esse construtor padrão pode ser mudado.

Podemos acessar os campos de a, fazendo:


In [6]:
a.bar


Out[6]:
"Texto"

In [7]:
a.número


Out[7]:
1234

Para criar um subtipo concreto de Pessoa, fazemos:


In [8]:
type Brasileiro <: Pessoa
    nome::String
    RG::Int
    vivo::Bool
end

In [9]:
super(Brasileiro) #Supertipo de Brasileiro


Out[9]:
Pessoa

In [10]:
brazuca = Brasileiro("Paulo", 12345, true)


Out[10]:
Brasileiro("Paulo",12345,true)

Seria conveniente criarmos um atalho para construir Brasileiros com o campo vivo == true. Muito simples, basta fazermos:


In [11]:
Brasileiro(nome, RG) = Brasileiro(nome, RG, true)


Out[11]:
Brasileiro (constructor with 3 methods)

In [12]:
outro_brazuca = Brasileiro("Artur", 12345)


Out[12]:
Brasileiro("Artur",12345,true)

Tipo ModInt

Nossa intenção nessa seção é criar um tipo que se comporta da seguinte maneira: ao criarmos um objeto Mod2, passamos um número n, porém o que fica guardado no campo valor deste objeto será n módulo 2 (ou seja, n % 2).

Para fazermos isso, usaremos uma marotagem:

Por padrão temos a função construtora Mod2(x) que pega o argumento x e coloca no campo valor do objeto. Nós trocaremos essa função padrão por uma função contrutora Mod2(x) que recebe um argumento x e guarda x % 2 no campo valor do objeto.


In [13]:
type Mod2 <: Integer
    valor::Int
    Mod2(valor) = new(valor % 2) #Nova função construtora
end

Observe como criamos novos objetos desse tipo:


In [14]:
Mod2(0)


Out[14]:
Mod2(0)

In [15]:
Mod2(1)


Out[15]:
Mod2(1)

In [16]:
Mod2(2)


Out[16]:
Mod2(0)

In [17]:
Mod2(1123123)


Out[17]:
Mod2(1)

Mas porque precisamos ficar limitador ao módulo 2? Porque não podemos fazer tipos módulo k, para qualquer k inteiro?

Ao invés de criarmos um tipo para cada k, podemos criar um tipo parametrizado por k. Ou seja, teremos um tipo ModInt{k} para cada valor de k.


In [18]:
type ModInt{k} <: Integer
    valor::Int
    ModInt(valor) = new(valor % k)
end

In [19]:
ModInt{2}(2)


Out[19]:
ModInt{2}(0)

In [20]:
ModInt{5}(3123)


Out[20]:
ModInt{5}(3)

Observe que cada ModInt{k} é um tipo por si mesmo:


In [21]:
v = ModInt{10}(12312)
typeof(v)


Out[21]:
ModInt{10} (constructor with 1 method)

Mas chamar ModInt{k}(valor) toda vez que quisermos criar um objeto desse tipo é um saco. Por isso, podemos criar um atalho, e fazer um construtor do tipo ModInt(k, valor):


In [22]:
ModInt(valor, k) = ModInt{k}(valor)


Out[22]:
ModInt{k} (constructor with 1 method)

In [23]:
ModInt(11,5)


Out[23]:
ModInt{5}(1)

Mas ainda temos um problema: toda vez que imprimimos o valor de ModInt{k}, na tela aparece ModInt{k}(valor). Convenhamos que esse padrão é muito feio.

Podemos imprimir esse tipo na tela de maneira mais bonita:


In [24]:
importall Base

#Ensina a mostrar ModInt{k} na tela
show{k}(io::IO, n::ModInt{k}) = print(io, "$(n.valor) mod $k")


Out[24]:
show (generic function with 90 methods)

Mas os objetos ModInt{k} ainda não podem ser somados, subtraidos, etc.


In [25]:
ModInt(11,5) #Deve mostrar algo mais bonito agora


Out[25]:
1 mod 5

In [26]:
ModInt(11,5) + ModInt(1,5)


+ not defined for ModInt{5}
while loading In[26], in expression starting on line 1

 in + at promotion.jl:188

Como definimos isso em Julia? Simples, basta definir as funções +, -, e * para esses tipos


In [27]:
#Ensina cada ModInt{k} a somar, subtrair e multiplicar
#Cria cada método para cada valor de k
-{k}(a::ModInt{k}) = ModInt{k}(-a.value)
-{k}(a::ModInt{k}, b::ModInt{k}) = ModInt{k}(a.valor - b.valor)
+{k}(a::ModInt{k}, b::ModInt{k}) = ModInt{k}(a.valor + b.valor)
*{k}(a::ModInt{k}, b::ModInt{k}) = ModInt{k}(a.valor * b.valor)


Out[27]:
* (generic function with 127 methods)

Podemos agora fazer as operações:


In [28]:
ModInt(11,5) + ModInt(1,5)


Out[28]:
2 mod 5

In [29]:
ModInt(11,5) * ModInt(1,5)


Out[29]:
1 mod 5

Porém os objetos do tipo ModInt{K} ainda não podem somar com outros tipos, por exemplo, Ints.

Quando somamos, subtraímos, multiplicamos ou dividimos dois subtipos diferentes de Integer, como ModInt{k} e Int, Julia tenta achar um tipo T em comum, e converter os tipos para esse tipo T. No nosso caso, o tipo em comum será o próprio ModInt{k}, portanto apenar o Int precisa ser convertido:


In [30]:
promote_rule{k}(::Type{ModInt{k}}, ::Type{Int}) = ModInt{k}


Out[30]:
promote_rule (generic function with 112 methods)

Porém, ainda não podemos somar os tipos, pois Julia não sabe como fazer a conversão de Int para ModInt{k}. Para ensinar Julia a fazer essa conversão, basta definir a função convert:


In [31]:
convert{k}(::Type{ModInt{k}}, i::Int) = ModInt{k}(i)


Out[31]:
convert (generic function with 445 methods)

Agora Julia sabe para qual tipo em comum deve promover Int e ModInt{k}, e como converter Int para ModInt{k}. Já temos todos os ingredientes necessários para fazer as operações aritméticas com esses dois tipos:


In [32]:
ModInt(5,2) + 2


Out[32]:
1 mod 2

In [33]:
ModInt(5,2) * 2


Out[33]:
0 mod 2

É importante notar que essa promoção é feita automaticamente apenas para as operações +, -, * e / e apenas para números (subtipos de Number). Para quaisquer outras operações, essa promoção deve ser definida pelo usuário.